package cmich.cps.scheduler;

import java.awt.Desktop;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import javax.swing.JTable;

import com.avaje.ebean.Ebean;

import cmich.cps.scheduler.data.AssignedCourse;
import cmich.cps.scheduler.data.Course;
import cmich.cps.scheduler.data.Preference;
import cmich.cps.scheduler.data.Professor;
import cmich.cps.scheduler.data.Room;
import cmich.cps.scheduler.data.Schedule;
import cmich.cps.scheduler.gui.Frame;
import cmich.cps.scheduler.gui.Prompts;

/**
 * The Class SchedulingSystem.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
public class SchedulingSystem {

	/** The Constant LOGGER. */
	private final static Logger LOGGER = Logger.getLogger(SchedulingSystem.class.getName());

	/** The courses. */
	private List<Course> courses;

	/** The rooms. */
	private List<Room> rooms;

	/** The timeslots. */
	private List<String> timeslots;

	/** The professors. */
	private List<Professor> professors;

	/** The schedules. */
	private List<Schedule> schedules;

	/** The master schedule. */
	private Schedule masterSchedule;

	/** The prompts. */
	private Prompts prompts;

	/** The Constant instance. */
	private final static SchedulingSystem instance = new SchedulingSystem();

	/**
	 * Instantiates a new scheduling system.
	 */
	private SchedulingSystem() {}

	/**
	 * Gets the single instance of SchedulingSystem.
	 *
	 * @return single instance of SchedulingSystem
	 */
	public static SchedulingSystem getInstance() {
		return instance;
	}

	/**
	 * Initializes some variables.
	 */
	public void init() {
		LOGGER.fine("Beginning system initialization.");
		prompts = Prompts.getInstance();
	}

	/**
	 * Load variables from database.
	 */
	public void loadDatabase() {
		setCourses();
		setRooms();
		setTimeslots();
		setProfessors();
		setSchedules();
	}

	/**
	 * Adds a course.
	 *
	 * @return true, if successful
	 */
	public boolean addCourse() {
		String designator = prompts.getInput("Enter the course designator:", "CPS");
		if(designator==null || designator.isEmpty()) {
			return false;
		}
		String number = prompts.getInput("Enter the course number:", "");
		if(number==null || number.isEmpty()) {
			return false;
		}
		String sectionNumber = prompts.getInput("Enter the course section number:", "");
		if(sectionNumber==null || sectionNumber.isEmpty()) {
			return false;
		}
		if(isDuplicate(sectionNumber, getCourses())) {
			String msg = "A course with section number " + sectionNumber + " already exists, add cancelled.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		Boolean hasLab = prompts.confirm("Does " + designator + " " + number + " have a lab attached to it?");
		Course course = new Course();
		course.setDesignator(designator);
		course.setNumber(number);
		course.setSectionNumber(sectionNumber);
		course.setHasLab(hasLab);
		course.setRoomPreference("Any");
		course.setEnrollment(0);
		this.courses.add(course);
		setCourses();
		LOGGER.fine("Added course " + designator + " " + number + " with section number " + sectionNumber);
		return true;
	}

	/**
	 * Modifies a course.
	 *
	 * @param row the row, represents the index of the course that changed
	 * @param col the col, represents the field that changed
	 * @param object the object, the new value for the changed field
	 * @return true, if successful
	 */
	public boolean modifyCourse(int row, int col, Object object) {
		Course course = this.courses.get(row);
		if(col==0) {
			course.setDesignator((String) object);
		}
		if(col==1) {
			course.setNumber((String) object);
		}
		if(col==2) {
			String sectionNumber = (String) object;
			if(!course.getSectionNumber().equals(sectionNumber) && isDuplicate(sectionNumber, getCourses())) {
				String msg = "A course with section number " + sectionNumber + " already exists. Reverting changes.";
				LOGGER.info(msg);
				prompts.nonFatalError(msg);
				return false;
			}
			course.setSectionNumber(sectionNumber);
		}
		if(col==3) {
			course.setHasLab((Boolean) object);
		}
		if(col==4) {
			course.setEnrollment((Integer) object);
		}
		if(col==5) {
			course.setRoomPreference((String) object);
		}
		setCourses();
		LOGGER.fine("Modified course " + course.getName() + " with section number " + course.getSectionNumber());
		return true;
	}

	/**
	 * Copies a course with a new section number.
	 * This is a convenience for the user.
	 *
	 * @param course the course to be copied
	 * @return true, if successful
	 */
	public boolean copyCourse(Course course) {
		String sectionNumber = prompts.getInput("Enter the new section number:", course.getSectionNumber());
		if(sectionNumber==null) {
			return false;
		}
		if(isDuplicate(sectionNumber, getCourses())) {
			String msg = "A course with section number " + sectionNumber + " already exists, copy cancelled.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		Course copy = new Course();
		copy.setDesignator(course.getDesignator());
		copy.setNumber(course.getNumber());
		copy.setSectionNumber(sectionNumber);
		copy.setHasLab(course.getHasLab());
		copy.setRoomPreference(course.getRoomPreference());
		copy.setEnrollment(course.getEnrollment());
		this.courses.add(copy);
		setCourses();
		LOGGER.fine("Copied course " + copy.getName() + ", new section number: " + sectionNumber);
		return true;
	}

	/**
	 * Removes the course.
	 *
	 * @param index the index of the course to be removed
	 */
	public void removeCourse(int index) {
		Course course = this.courses.get(index);
		LOGGER.fine("Removing course " + course.getName() + " with section number " + course.getSectionNumber());
		this.courses.remove(course);
		Ebean.delete(course);
	}

	/**
	 * Gets the courses.
	 *
	 * @return the courses
	 */
	public List<Course> getCourses() {
		return this.courses;
	}

	/**
	 * Sets the courses.
	 */
	public void setCourses() {
		if(this.courses!=null) {
			Ebean.save(courses);
			LOGGER.finest("Saved courses to database.");
		}
		this.courses = Ebean.find(Course.class).findList();
		LOGGER.finest("Loaded courses from database.");
	}

	/**
	 * Adds a room.
	 *
	 * @return true, if successful
	 */
	public boolean addRoom() {
		String building = prompts.getInput("Enter the building:", "PE");
		if(building==null) {
			return false;
		}
		String number = prompts.getInput("Enter the room number:", "");
		if(number==null) {
			return false;
		}
		if(isDuplicate(building + " " + number, getRooms())) {
			String msg = "Room " + number + " already exists in building " + building + ", add cancelled.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		Boolean isLab = prompts.confirm("Is " + building + " " + number + " a lab?");
		Room room = new Room();
		room.setBuilding(building);
		room.setNumber(number);
		room.setLab(isLab);
		room.setCapacity(0);
		this.rooms.add(room);
		setRooms();
		LOGGER.fine("Added room " + room);
		return true;
	}

	/**
	 * Modifies a room.
	 *
	 * @param row the row, represents the index of the room that changed
	 * @param col the col, represents the field that changed
	 * @param object the object, the new value for the changed field
	 * @return true, if successful
	 */
	public boolean modifyRoom(int row, int col, Object object) {
		Room room = this.rooms.get(row);
		if(col==0) {
			String building = (String) object;
			String name = building + " " + room.getNumber();
			if(!room.getName().equals(name) && isDuplicate(name, getRooms())) {
				String msg = "Room " + room.getNumber() + " already exists in building " + building + ". Reverting changes.";
				LOGGER.info(msg);
				prompts.nonFatalError(msg);
				return false;
			}
			room.setBuilding(building);
		}
		if(col==1) {
			String number = (String) object;
			String name = room.getBuilding() + " " + number;
			if(!room.getName().equals(name) && isDuplicate(name, getRooms())) {
				String msg = "Room " + number + " already exists in building " + room.getBuilding() + ". Reverting changes.";
				LOGGER.info(msg);
				prompts.nonFatalError(msg);
				return false;
			}
			room.setNumber(number);
		}
		if(col==2) {
			room.setCapacity((Integer) object);
		}
		if(col==3) {
			room.setLab((Boolean) object);
		}
		setRooms();
		return true;
	}

	/**
	 * Removes a room.
	 *
	 * @param index the index of the room to be removed
	 */
	public void removeRoom(int index) {
		Room room = this.rooms.get(index);
		LOGGER.fine("Removing room " + room);
		this.rooms.remove(room);
		Ebean.delete(room);
	}

	/**
	 * Gets the rooms.
	 *
	 * @return the rooms
	 */
	public List<Room> getRooms() {
		return this.rooms;
	}

	/**
	 * Sets the rooms.
	 */
	public void setRooms() {
		if(this.rooms!=null) {
			Ebean.save(rooms);
			LOGGER.finest("Saved rooms to database.");
		}
		this.rooms = Ebean.find(Room.class).findList();
		LOGGER.finest("Loaded rooms from database.");
	}

	/**
	 * Gets the timeslots.
	 *
	 * @return the timeslots
	 */
	public List<String> getTimeslots() {
		return this.timeslots;
	}

	/**
	 * Sets the timeslots.
	 */
	public void setTimeslots() {
		LOGGER.fine("Setting standard timeslots.");
		this.timeslots = new ArrayList<String>();
		//MWF Timeslots
		this.timeslots.add("MWF 8:00-8:50");
		this.timeslots.add("MWF 9:00-9:50");
		this.timeslots.add("MWF 10:00-10:50");
		this.timeslots.add("MWF 11:00-11:50");
		this.timeslots.add("MWF 12:00-12:50");
		this.timeslots.add("MWF 1:00-1:50");
		this.timeslots.add("MWF 2:00-3:15");
		this.timeslots.add("MWF 3:30-4:45");
		this.timeslots.add("MWF 5:00-6:15");
		//TR Timeslots
		this.timeslots.add("TR 8:00-9:15");
		this.timeslots.add("TR 9:30-10:45");
		this.timeslots.add("TR 11:00-12:15");
		this.timeslots.add("TR 12:30-1:45");
		this.timeslots.add("TR 2:00-3:15");
		this.timeslots.add("TR 3:30-4:45");
		this.timeslots.add("TR 5:00-6:15");
	}

	/**
	 * Adds a professor.
	 *
	 * @return true, if successful
	 */
	public boolean addProfessor() {
		String name = prompts.getInput("Enter the professor's name:", "");
		if(name==null) {
			return false;
		}
		if(isDuplicate(name, getProfessors())) {
			String msg = "Professor " + name + " already exists, add cancelled.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		String maxCoursesString = prompts.getInput("Maximum courses for " + name + ":", "");
		if(maxCoursesString==null) {
			return false;
		}
		int maxCourses = 0;
		try {
			maxCourses = Integer.parseInt(maxCoursesString);
		} catch(NumberFormatException e) {
			String msg = "The maximum courses should be an integer, " + maxCoursesString + " is not an integer.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		if(maxCourses<1) {
			String msg = "A professor cannot have a max courses value less than 1.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		Professor professor = new Professor();
		professor.setName(name);
		professor.setTimeBetweenCourses("No preference");
		professor.setMaxCourses(maxCourses);
		this.professors.add(professor);
		setProfessors();
		LOGGER.fine("Added professor " + professor);
		return true;
	}

	/**
	 * Modifies a professor.
	 *
	 * @param row the row, represents the index of the professor that changed
	 * @param col the col, represents the field that changed
	 * @param object the object, the new value for the changed field
	 * @return true, if successful
	 */
	public boolean modifyProfessor(int row, int col, Object object) {
		Professor professor = this.professors.get(row);
		if(col == 0) {
			String name = (String) object;
			if(!professor.getName().equals(name) && isDuplicate(name, getProfessors())) {
				String msg = "Professor " + name + " already exists. Reverting changes.";
				LOGGER.info(msg);
				prompts.nonFatalError(msg);
				return false;
			}
			professor.setName(name);
			Frame.getInstance().closeTab(name);
		}
		if(col==1) {
			String timeBetweenCourses = (String) object;
			professor.setTimeBetweenCourses(timeBetweenCourses);
		}
		if(col==2) {
			int maxCourses = (Integer) object;
			if(maxCourses<1) {
				String msg = "A professor cannot have a max courses value less than 1. Reverting changes.";
				LOGGER.fine(msg);
				prompts.nonFatalError(msg);
				return false;
			}
			professor.setMaxCourses(maxCourses);
		}
		setProfessors();
		return true;
	}

	/**
	 * Removes a professor.
	 *
	 * @param index the index of the professor to be removed
	 */
	public void removeProfessor(int index) {
		Professor professor = this.professors.get(index);
		LOGGER.fine("Removing professor " + professor);
		this.professors.remove(professor);
		Ebean.delete(professor);
	}

	/**
	 * Gets the professors.
	 *
	 * @return the professors
	 */
	public List<Professor> getProfessors() {
		return this.professors;
	}

	/**
	 * Sets the professors.
	 */
	public void setProfessors() {
		if(this.professors!=null) {
			Ebean.save(professors);
			LOGGER.finest("Saved professors to database.");
		}
		this.professors = Ebean.find(Professor.class)
				.fetch("preferences")
				.findList();
		LOGGER.finest("Loaded professors from database.");
	}

	/**
	 * Adds a preference.
	 *
	 * @param name the name of the professor to add the preference to
	 * @return true, if successful
	 */
	public boolean addPreference(String name) {
		Professor professor = getProfessorByName(name);
		String category = prompts.getCategory();
		if(category==null) { return false; }
		String[] choices = setPreferenceChoices(category);
		Arrays.sort(choices);
		if(choices == null || choices.length == 0) {
			String msg = "There are no " + category + "s to add a preference for.  Add the " + category + " first then add the preference.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		String item = prompts.getItem(choices);
		if(item==null) { return false; }
		if(isDuplicate(item, professor.getPreferences())) {
			String msg = professor + " already has a preference for " + item;
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		int score = setPreferenceValue(professor, item);
		if(score==0) { return false; }
		Preference preference = new Preference();
		preference.setItem(item);
		preference.setValue(score);
		professor.addPreference(preference);
		setProfessors();
		professor = getProfessorByName(name);
		LOGGER.fine("Added preference to " + professor + " for " + item + " with score of " + score);
		return true;
	}

	/**
	 * Sets the preference value.
	 *
	 * @param professor the professor
	 * @param item the item
	 * @return the preference score
	 */
	private int setPreferenceValue(Professor professor, String item) {
		String scoreString = prompts.getInput("Enter an integer value representing the strength of the preference:\n" +
				"-100 to 100, 0=No preference", "");
		if(scoreString==null) return 0;
		int score = 0;
		try {
			score = Integer.parseInt(scoreString);
			if(score < -100 || score > 100) {
				String msg = "The score must be between -100 and 100;  " + score + " is outside this range.";
				LOGGER.info(msg);
				prompts.nonFatalError(msg);
				return 0;
			}
			if(score==0) {
				String msg = "A score of 0 is equal to no preference and therefore will be discarded.";
				LOGGER.fine(msg);
				prompts.message(msg);
				return 0;
			}
		} catch(NumberFormatException e) {
			String msg = "The score must be an integer, " + scoreString + " is not an integer.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return 0;
		}
		return score;
	}

	/**
	 * Sets the preference choices, given a category of preferences. 
	 * One of courses, rooms, or timeslots.
	 *
	 * @param category the category
	 * @return the string[] representing possible choices
	 */
	private String[] setPreferenceChoices(String category) {
		String[] choices = null;
		if(category.equals("course")) {
			List<String> choiceList = new ArrayList<String>();
			for(int i=0;i<this.courses.size();i++) {
				String name = this.courses.get(i).getName();
				if(!choiceList.contains(name)) {
					choiceList.add(name);
				}
			}
			choices = (String[])choiceList.toArray(new String[choiceList.size()]);
		}
		if(category.equals("room")) {
			choices = new String[this.rooms.size()];
			for(int i=0;i<this.rooms.size();i++) {
				choices[i] = this.rooms.get(i).getName();
			}
		}
		if(category.equals("timeslot")) {
			choices = new String[this.timeslots.size()];
			for(int i=0;i<this.timeslots.size();i++) {
				choices[i] = this.timeslots.get(i);
			}
		}
		return choices;
	}

	/**
	 * Modifies a preference.
	 *
	 * @param name the name of the professor to which the preference belongs
	 * @param row the row, represents the index of the preference that changed
	 * @param col the col, represents the field that changed
	 * @param object the object, the new value for the changed field
	 * @return true, if successful
	 */
	public boolean modifyPreference(String name, int row, int col, Object object) {
		Professor professor = getProfessorByName(name);
		Preference preference = professor.getPreferences().get(row);
		if(col == 0) {
			String item = (String) object;
			if(!item.equalsIgnoreCase(professor.getPreferences().get(row).getItem()) && isDuplicate(item, professor.getPreferences())) {
				String msg = professor + " already has a preference for " + item + ". Reverting changes.";
				LOGGER.info(msg);
				prompts.nonFatalError(msg);
				return false;
			}
			preference.setItem(item);
		}
		if(col == 1) {
			int value = (Integer) object;
			if(value < -100 || value > 100) {
				String msg = value + " is not between -100 and 100. Reverting changes.";
				LOGGER.info(msg);
				prompts.nonFatalError(msg);
				return false;
			}
			preference.setValue(value);
		}
		setProfessors();
		professor = getProfessorByName(name);
		LOGGER.info("Modified " + professor.getName() + "'s preference for " + preference.getItem() + " to " + preference.getValue());
		return true;
	}

	/**
	 * Removes a preference.
	 *
	 * @param name the name of the professor to which the preference belongs
	 * @param index the index of the preference
	 */
	public void removePreference(String name, int index) {
		Professor professor = getProfessorByName(name);
		Preference preference = professor.getPreferences().get(index);
		LOGGER.fine("Removing preference " + preference.getItem() + " from " + professor.getName());
		professor.removePreference(preference);
		Ebean.delete(preference);
		setProfessors();
	}

	/**
	 * Adds a schedule.
	 *
	 * @param schedule the schedule
	 */
	public void addSchedule(Schedule schedule) {
		LOGGER.fine("Adding generated schedule " + schedule.getId());
		this.schedules.add(schedule);
		setSchedules();
	}

	/**
	 * Checks if <code>item</code> is already in <code>objects</code>.
	 *
	 * @param item the item to be checked
	 * @param objects the list of objects to check
	 * @return true, if is a duplicate
	 */
	private boolean isDuplicate(String item, List<?> objects) {
		for(int i=0;i<objects.size();i++) {
			if(objects.get(i).toString().equalsIgnoreCase(item)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Removes a schedule.
	 *
	 * @param index the index of the schedule to be removed
	 */
	public boolean removeSchedule(int index) {
		if(prompts.confirm("Are you sure you want to permanently delete this schedule?")) {
			Schedule schedule = this.schedules.get(index);
			LOGGER.fine("Removing schedule " + schedule.getId());
			this.schedules.remove(schedule);
			Ebean.delete(schedule);
			return true;
		}
		return false;
	}

	/**
	 * Gets the schedules.
	 *
	 * @return the schedules
	 */
	public List<Schedule> getSchedules() {
		return this.schedules;
	}

	/**
	 * Sets the schedules.
	 */
	public void setSchedules() {
		if(this.schedules!=null) {
			Ebean.save(this.schedules);
			LOGGER.finest("Saved " + this.schedules.size() + " schedules to database.");
		}
		this.schedules = Ebean.find(Schedule.class).where().ne("algorithmUsed", "Master").findList();
		LOGGER.finest("Loaded " + this.schedules.size() + " schedules from database.");
	}

	/**
	 * Gets the master schedule.
	 *
	 * @return the master schedule
	 */
	public Schedule getMasterSchedule() {
		this.masterSchedule = Ebean.find(Schedule.class).where().eq("algorithmUsed", "Master").findUnique();
		if(this.masterSchedule==null) {
			Schedule schedule = new Schedule();
			schedule.setAlgorithmUsed("Master");
			schedule.setValue(0);
			this.masterSchedule = schedule;
			Ebean.save(this.masterSchedule);
		}
		return this.masterSchedule;
	}

	/**
	 * Gets the professor by name.
	 *
	 * @param name the name
	 * @return the professor whose name matches <code>name</code>
	 */
	public Professor getProfessorByName(String name) {
		for(int i=0;i<this.professors.size();i++) {
			Professor professor = this.professors.get(i);
			if(professor.getName().equals(name)) {
				return professor;
			}
		}
		return null;
	}

	/**
	 * Removes the manually assigned course.
	 *
	 * @param index the index of the course to be removed
	 */
	public void removeManuallyAssignedCourse(int index) {
		AssignedCourse course = getMasterSchedule().getCourses().get(index);
		getMasterSchedule().getCourses().remove(course);
		Ebean.delete(course);	
	}

	/**
	 * Adds a manually assigned course.
	 *
	 * @return true, if successful
	 */
	public boolean addManuallyAssignedCourse() {
		//Choose course
		List<Course> courses = filterCourses();
		if(courses.size()==0) {
			String msg = "No more courses available to be manually assigned.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		String course = getChoice(courses);
		if(course==null) {
			return false;
		}
		String courseName = course.split(" Section: ")[0];
		String sectionNumber = course.split(" Section: ")[1];
		//Choose professor
		List<Professor> professors = filterProfessors();
		if(professors.size()==0) {
			String msg = "No more professors available to be manually assigned.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		String professorName = getChoice(professors);
		if(professorName==null) {
			return false;
		}
		//Choose room
		List<Room> rooms = filterRooms(professorName);
		if(rooms.size()==0) {
			String msg = "No more rooms available to be manually assigned.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		String roomName = getChoice(rooms);
		if(roomName==null) {
			return false;
		}
		//Choose time slot
		String timeslot = null;
		for(int i=0;i<rooms.size();i++) {
			Room room = rooms.get(i);
			if(room.getName().equals(roomName)) {
				timeslot = getChoice(room.getTimeslots());
			}
		}
		if(timeslot==null) {
			return false;
		}
		AssignedCourse assignedCourse = new AssignedCourse();
		assignedCourse.setName(courseName);
		assignedCourse.setSectionNumber(sectionNumber);
		assignedCourse.setProfessor(professorName);
		assignedCourse.setRoom(roomName);
		assignedCourse.setTimeslot(timeslot);
		Schedule schedule = getMasterSchedule();
		schedule.addCourse(assignedCourse);
		Ebean.save(schedule);
		return true;
	}

	/**
	 * Filters {@link #courses} to those that are not yet assigned.
	 *
	 * @return the list of filtered courses
	 * @see #addManuallyAssignedCourse()
	 */
	private List<Course> filterCourses() {
		List<Course> courses = new ArrayList<Course>();
		courses.addAll(this.courses);
		Schedule schedule = getMasterSchedule();
		for(AssignedCourse course : schedule.getCourses()) {
			for(int i=0;i<courses.size();i++) {
				if(courses.get(i).getSectionNumber().equals(course.getSectionNumber())) {
					courses.remove(i);
				}
			}
		}
		return courses;
	}

	/**
	 * Filters {@link #professors} to those that are not yet assigned.
	 *
	 * @return the list of filtered professors
	 * @see #addManuallyAssignedCourse()
	 */
	private List<Professor> filterProfessors() {
		List<Professor> professors = new ArrayList<Professor>();
		professors.addAll(this.professors);
		for(int i=0;i<professors.size();i++) {
			professors.get(i).setAssignedCourses(0);
		}
		Schedule schedule = getMasterSchedule();
		for(AssignedCourse course : schedule.getCourses()) {
			for(int i=0;i<professors.size();i++) {
				if(professors.get(i).getName().equals(course.getProfessor())) {
					professors.get(i).setAssignedCourses(professors.get(i).getAssignedCourses()+1);
				}
				if(professors.get(i).getAssignedCourses()==professors.get(i).getMaxCourses()) {
					professors.remove(i);
				}
			}
		}
		return professors;
	}

	/**
	 * Filters {@link #rooms} to those that are not yet assigned.
	 *
	 * @param professorName the professor name
	 * @return the list of filtered rooms
	 * @see #addManuallyAssignedCourse()
	 */
	private List<Room> filterRooms(String professorName) {
		List<Room> rooms = new ArrayList<Room>();
		rooms.addAll(this.rooms);
		for(int i=0;i<rooms.size();i++) {
			Room room = rooms.get(i);
			room.setTimeslots(this.timeslots);
		}
		Schedule schedule = getMasterSchedule();
		for(AssignedCourse course : schedule.getCourses()) {
			if(course.getProfessor().equals(professorName)) {
				for(Room room : rooms) {
					room.removeTimeslot(course.getTimeslot());
				}
			}
			for(int i=0;i<rooms.size();i++) {
				Room room = rooms.get(i);
				if(room.getName().equals(course.getRoom())) {
					room.removeTimeslot(course.getTimeslot());
					if(room.getTimeslots().size()==0) {
						rooms.remove(i);
					}
				}
			}
		}
		return rooms;
	}

	/**
	 * Presents a <code>list</code> of choices to the user and returns their choice.
	 *
	 * @param list the list of choices
	 * @return the user's choice
	 * @see #addManuallyAssignedCourse()
	 */
	private String getChoice(List<?> list) {
		String[] choices = new String[list.size()];
		for(int i=0;i<list.size();i++) {
			Object object = list.get(i);
			if(object instanceof Course) {
				Course course = (Course) object;
				choices[i] = course.getName() + " Section: " + course.getSectionNumber();
			}
			if(object instanceof Professor) {
				Professor professor = (Professor) object;
				choices[i] = professor.getName();
			}
			if(object instanceof Room) {
				Room room = (Room) object;
				choices[i] = room.getName();
			}
			if(object instanceof String) {
				choices[i] = (String) object;
			}
		}
		return prompts.getItem(choices);
	}

	/**
	 * Exports a schedule to HTML.
	 *
	 * @param schedule the schedule to be exported
	 */
	public void exportScheduleToHTML(JTable table, Schedule schedule) {
		LOGGER.fine("Generating result file.");
		String timestamp = getTimestamp();
		String fileName = "Schedule - " + schedule.getAlgorithmUsed() + " - " + timestamp;
		File file = new File(fileName + ".htm");
		try {
			FileWriter html = new FileWriter(file);
			html.write("<!DOCTYPE html>\n<html>\n");
			html.write("<head>\n");
			html.write("<title>" + fileName + "</title>\n");
			html.write("<style type=\"text/css\">\n" +
					"table,th,td {border: 1px solid black}\n" +
					"td {height:100%}\n" +
					"</style>\n");
			html.write("</head>\n");
			html.write("<body>\n<table>\n");
			html.write("<tr>");
			for(int i = 0;i<table.getColumnCount();i++){
				html.write("<th>" + table.getColumnName(i) + "</th>");
			}
			html.write("</tr>");
			for(int i=0;i<table.getRowCount();i++) {
				html.write("<tr>");
				for(int j=0;j<table.getColumnCount();j++) {
					String cell = "" + table.getValueAt(i,j);
					cell = cell.replace("null", " ");
					html.write("<td>" + cell + "</td>");
				}
				html.write("</tr>\n");
			}
			html.write("</table>\n</body>\n</html>");
			html.close();
			displayFile(file);
		} catch (IOException e) {
			String msg = "Error writing schedule to file: " + e.toString();
			LOGGER.warning(msg);
			prompts.nonFatalError(msg);
		}
	}

	/**
	 * Displays a schedule in the browser after exporting it to HTML.
	 *
	 * @param file the HTML schedule file
	 * @see #exportScheduleToHTML(Schedule)
	 */
	private void displayFile(File file) {
		if(file!=null && prompts.confirm("Would you like to open the exported file now?")) {
			try {
				if(file.getName().endsWith(".htm")) {
					Desktop.getDesktop().browse(file.toURI());
				}
				else {
					Desktop.getDesktop().open(file);
				}
			} catch (IOException e) {
				String msg = "Unable to open exported schedule: " + e.toString();
				LOGGER.warning(msg);
				prompts.nonFatalError(msg);
			}
		}
	}

	/**
	 * Export table to tsv.
	 *
	 * @param table the table
	 * @param schedule the schedule
	 */
	public void exportTableToTSV(JTable table, Schedule schedule) {
		LOGGER.fine("Generating result file.");
		String timestamp = getTimestamp();
		String fileName = "Schedule - " + schedule.getAlgorithmUsed() + " - " + timestamp;
		File file = new File(fileName + ".tsv");
		try {
			FileWriter tsv = new FileWriter(file);
			for(int i = 0;i<table.getColumnCount();i++){
				tsv.write(table.getColumnName(i) + "\t");
			}
			tsv.write("\n");
			for(int i=0;i<table.getRowCount();i++) {
				for(int j=0;j<table.getColumnCount();j++) {
					String cell = "" + table.getValueAt(i,j);
					cell = cell.replace("null", " ");
					tsv.write(cell + "\t");
				}
				tsv.write("\n");
			}
			tsv.close();
		} catch (IOException e) {
			String msg = "Failed to export to spreadsheet: " + e.toString();
			LOGGER.warning(msg);
			prompts.nonFatalError(msg);
		}	
	}

	/**
	 * Gets the current time as a string.
	 *
	 * @return the current timestamp
	 */
	private String getTimestamp() {
		Date date = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd h-mm a");
		String timestamp = sdf.format(date);
		return timestamp;
	}

	/**
	 * Clears all manually assigned courses.
	 * 
	 * @return true, if successful
	 */
	public boolean clearManualCourses() {
		if(prompts.confirm("This will erase all manually assigned courses, are you sure you want to do this?")) {
			Schedule masterSchedule = getMasterSchedule();
			Ebean.delete(masterSchedule.getCourses());
			getMasterSchedule().getCourses().clear();
			return true;
		}
		return false;
	}

	/**
	 * Imports all assigned courses in schedule at the given <code>index</code> to the
	 * master schedule, making them manually assigned courses.
	 * 
	 * @param index of the schedule to import from in {@link #schedules}
	 * @return true, if successful
	 */
	public boolean importToMasterSchedule(int index) {
		if(clearManualCourses()) {
			Schedule master = getMasterSchedule();
			Schedule schedule = this.schedules.get(index);
			for(AssignedCourse course : schedule.getCourses()) {
				AssignedCourse newCourse = new AssignedCourse();
				newCourse.setEnrollment(course.getEnrollment());
				newCourse.setLab(course.getLab());
				newCourse.setName(course.getName());
				newCourse.setProfessor(course.getProfessor());
				newCourse.setRoom(course.getRoom());
				newCourse.setSectionNumber(course.getSectionNumber());
				newCourse.setTimeslot(course.getTimeslot());
				master.addCourse(newCourse);
			}
			Ebean.save(master);
			return true;
		}
		return false;
	}
}